/*
 * $Id: Noid.java 3 2007-12-07 01:24:30Z rasan.rasch $
 *
 * Copyright (c) 2002-2006 UC Regents
 * 
 * Permission to use, copy, modify, distribute, and sell this software and
 * its documentation for any purpose is hereby granted without fee, provided
 * that (i) the above copyright notices and this permission notice appear in
 * all copies of the software and related documentation, and (ii) the names
 * of the UC Regents and the University of California are not used in any
 * advertising or publicity relating to the software without the specific,
 * prior written permission of the University of California.
 * 
 * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
 * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
 * 
 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE FOR ANY
 * SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY
 * THEORY OF LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE
 * OR PERFORMANCE OF THIS SOFTWARE.
 */

package org.cdl.noid;

import com.sleepycat.je.*;
import java.io.*;
import java.nio.channels.*;
import java.text.*;
import java.util.*;
import java.util.regex.*;

/** 
 * The <b>noid</b> utility creates minters (identifier generators) and
 * accepts commands that operate them.  Once created, a minter can be
 * used to produce persistent, globally unique names for documents,
 * databases, images, vocabulary terms, etc.  Properly managed, these
 * identifiers can be used as long term durable information object
 * references within naming schemes such as ARK, PURL, URN, DOI, and
 * LSID.  At the same time, alternative minters can be set up to
 * produce short-lived names for transaction identifiers, compact web
 * server session keys (cf. UUIDs), and other ephemera.
 * <p> 
 * In general, a <b>noid</b> minter efficiently generates, tracks, and
 * binds unique identifiers, which are produced without replacement in
 * random or sequential order, and with or without a check character
 * that can be used for detecting transcription errors.  A minter can
 * bind identifiers to arbitrary element names and element values that
 * are either stored or produced upon retrieval from rule-based
 * transformations of requested identifiers; the latter has application
 * in identifier resolution.  Noid minters are very fast, scalable,
 * easy to create and tear down, and have a relatively small footprint.
 * They use BerkeleyDB as the underlying database.
 * <p>
 * Identifiers generated by a <b>noid</b> minter are also known as
 * "noids" (nice opaque identifiers).  While a minter can record and
 * bind any identifiers that you bring to its attention, often it is
 * used to generate, bringing to your attention, identifier strings
 * that carry no widely recognizable meaning.  This semantic opaqueness
 * reduces their vulnerability to era- and language-specific change,
 * and helps persistence by making for identifiers that can age and
 * travel well.
 *
 * @author  John Kunze
 * @author  Rasan Rasch
 * @version $Revision: 3 $
 */
public class Noid {

	private static final int NOLIMIT = -1;
	private static final int SEQNUM_MIN = 1;
	private static final int SEQNUM_MAX = 1000000;

	private static final String VERSION = "$Revision: 3 $";
	
	private final char R = ':';

	private Hashtable<String,Integer> ord
		= new Hashtable<String,Integer>();

	private int alphaCount = 0;
	private int digitCount = 0;
	private int lockTest = 0;

	// Alphabet for identifier names
	private static final char[] alpha = {
		'0', '1', '2', '3', '4',
		'5', '6', '7', '8', '9',
		'b', 'c', 'd', 'f', 'g',
		'h', 'j', 'k', 'm', 'n',
		'p', 'q', 'r', 's', 't',
		'v', 'w', 'x', 'z'
	};

	private StringBuffer legalString = new StringBuffer(alpha.length);

	private HashMap<Character,String> legalMap
		= new HashMap<Character,String>();

	private HashMap<Character,String> digitMap
		= new HashMap<Character,String>();

	private static final String[] validHows = {
		"new",     "replace",    "set",
		"append",  "prepend",    "add",
		"insert",  "delete",     "purge",
		"mint",    "peppermint"
	};

	private String contact;
	private String dbHome;
	private NoidDB noidDB;

	private String msg;

	private static Sys sys;

	public Noid() {
		this.dbHome = ".";
		setup();
	}

	/**
	 * @param contact 
	 * @param dbHome 
	 */
	public Noid(String contact, String dbHome) {
		this.contact = contact;
		this.dbHome = dbHome;
		setup();
	}

	/**
	 * @param contact 
	 */
	public void setContact(String contact) {
		this.contact = contact;
	}

	/**
	 * @param dbHome 
	 */
	public void setDbHome(String dbHome) {
		this.dbHome = dbHome;
	}

	public String getDbHome() {
		return dbHome;
	}

	public String getDbFilePath() {
		return dbHome + "/dbnoid/noid.bdb";
	}

	/**
	 * @param sys 
	 */
	public void setSys(Sys sys) {
		this.sys = sys;
	}

	/**
	 * @param buf 
	 */
	public void addMsg(String buf) {
		System.err.println(buf);
		noidDB.addMsg(buf);
	}

	public String getMsg() {
		return noidDB.getMsg(false);
	}

	/**
	 * @param reset 
	 */
	public String getMsg(boolean reset) {
		return noidDB.getMsg(reset);
	}

	/**
	 * @param buf 
	 */
	public void writeLog(String buf) {
		noidDB.writeLog(buf);
	}

	/**
	 * @param buf 
	 */
	public void logMsg(String buf) {
		System.err.println(buf);
		noidDB.writeLog(buf);
	}

	public String getReadme () {
		return Util.getFile(dbHome + "/README");
	}

	/**
	 *
	 *
	 * @param validate 
	 * @param how 
	 * @param id 
	 * @param elem 
	 * @param value 
	 */
	public String bind(boolean validate,
				String how, String id, String elem, String value) {

	// yyy to add: incr, decr for $how;  possibly other ops (* + - / **)

		// Validate identifier and element if necessary.
		//
		// yyy to do: check $elem against controlled vocab
		//     (for errors more than for security)
		// yyy should this genonly setting be so capable of contradicting
		//     the $validate arg?
		String[] ids = { id };
		if (noidDB.rGetBool("genonly")
				&& validate && Util.isEmpty(validate("-", ids))) {
			return null;
		} else if (elem == null || elem.equals("")) {
			addMsg("error: bind needs an identifier specified.");
			return null;
		}
	
		if (elem == null || elem.equals("")) {
			addMsg("error: \"bind " + how
				+ "\" requires an element name.");
			return null;
		}

		// Transform and place a "hold" (if "long" term and we're not deleting)
		// on a special identifier.  Right now that means a user-entrered Id
		// of the form :idmap/Idpattern.  In this case, change it to a database
		// Id of the form "$R/idmap/$elem", and change $elem to hold Idpattern;
		// this makes lookup faster and easier.
		//
		// First save original id and element names in $oid and $oelem to
		// use for all user messages; we use whatever is in $id and $elem
		// for actual database operations.
		//
		String oid = id;
		String oelem = elem;
		int hold = 0;

		StringBuffer buf = new StringBuffer();
	
		if (!matches("^:", id)) {
			if (!matches("^:idmap/(.+)", id, 1, buf)) {
				addMsg("error: " + oid + ": id cannot begin with \":\""
					+ " unless of the form \":idmap/Idpattern\".");
				return null;
			}
			id = R + "/idmap/" + oelem;
			elem = buf.toString();
			if (noidDB.rGetBool("longterm"))
				hold = 1;
		}
	
		// yyy transform other ids beginning with ":"?

		// Check circulation status.  Error if term is "long" and the id
		// hasn't been issued unless a hold was placed on it.
		//
		// If no circ record and no hold...
		// 
		if (!noidDB.defined(id + "\t" + R + "/c")
				&& !noidDB.exists(id + "\t" + R + "/h")) {
			
			if (noidDB.rGetBool("longterm")) {
				addMsg("error: "
					+ oid + ": \"long\" term disallows binding "
					+ "an unissued identifier unless a hold is "
					+ "first placed on it.");
				return null;
			} else {
				logMsg("warning:"
					+ " " + oid + ": binding an unissued identifier"
					+ " that has no hold placed on it.");
			}
		}
	
		if (Util.grep("^" + how + "$", validHows).length != 1) {
			addMsg("error: bind how?  What does " + how + " mean?");
			return null;
		}
	
		boolean peppermint = how.equals("peppermint");

		if (peppermint) {
			// yyy to do
			addMsg("error: bind \"peppermint\" not implemented.");
			return null;
		}

		// YYY bind mint file Elem Value		-- put into FILE by itself
		// YYY bind mint stuff_into_big_file Elem Value -- cat into file
		if (how.equals("mint") || how.equals("peppermint")) {
			if (!id.equals("new")) {
				addMsg("error: bind \"mint\" requires id to be "
					+ "given as \"new\".");
				return null;
			}
			
			id = oid = mint(peppermint);
			if (id == null)
				return null;
		}

	
		if (how.equals("delete") || how.equals("purge")) {
			if (value != null && value.equals("")) {
				addMsg("error: why does \"bind " + how
					+ "have a supplied value (" + value + ")?");
				return null;
			}
			value = "";
		} else if (value == null) {
			addMsg( "error: \"bind " + how + " " + elem
				+ "\" requires a value to bind.");
			return null;
		}

		// If we get here, $value is defined and we can use with impunity.

		dbLock();

		
		if (!noidDB.defined(id + "\t" + elem)) {		// currently unbound
			String[] arr = { "replace", "append", "prepend", "delete" };
			if (Util.grep("^" + how + "$", arr).length == 1) {
				addMsg("error: for \"bind " + how +"\", \""
					+ oid + " " + oelem + "\" "
					+ "must already be bound.");
				dbUnlock();
				return null;
			}
			noidDB.set(id + "\t" + elem, "");	// can concatenate with impunity
		} else {						// currently bound
			String[] arr = { "new", "mint", "peppermint" };
			if (Util.grep("^" + how + "$", arr).length == 1) {
				addMsg("error: for \"bind " + how +"\", \""
					+ oid + " " + oelem + "\" "
					+ "cannot already be bound.");
				dbUnlock();
				return null;
			}
		}
	
		// We don't care about bound/unbound for:  set, add, insert, purge

		int oldlen = noidDB.get(id + "\t" + elem).length();
		int newlen = value.length();
		String statmsg = newlen + " bytes written";

		if (how.equals("delete") || how.equals("purge")) {
			noidDB.remove(id + "\t" + elem);
			statmsg = oldlen + " bytes removed";
		} else if (how.equals("add") || how.equals("append")) {
			noidDB.append(id + "\t" + elem,  value);
			statmsg += " to the end of " + oldlen + " bytes";
		} else if (how.equals("insert") || how.equals("prepend")) {
			noidDB.prepend(id + "\t" + elem,  value);
			statmsg += " to the beginning of " + oldlen + " bytes";
		} else {
			noidDB.set(id + "\t" + elem, value);
			statmsg += ", replacing " + oldlen + " bytes";
		}

		if (hold != 0
				&& noidDB.exists(id + "\t" + elem)
				&& !holdSet(id)) {
			hold = -1;	// don't just bail out -- we need to unlock
		}

		// yyy $contact info ?  mainly for "long" term identifiers?
		dbUnlock();

		return
// yyy should this $id be or not be $oid???
// yyy should labels for Id and Element be lowercased???
			"Id:      " + id   + "\n" +
			"Element: " + elem + "\n" +
			"Bind:    " + how  + "\n" +
			"Status:  " + (hold == -1 ? getMsg() : "ok, " + statmsg) + "\n";
	}

	void setup() {

		alphaCount = alpha.length;
		digitCount = 10;

		for (int i = 0; i < alpha.length; i++) {
			if (i <= 9) {
				digitMap.put(new Character(alpha[i]), "1");
			}
			legalString.append(alpha[i]);
			legalMap.put(new Character(alpha[i]), "1");
			ord.put(String.valueOf(alpha[i]), new Integer(i));
		}
		
	}

	// Compute check character for given identifier
	/**
	 * @param id 
	 */
	public String checkChar(String id) {
		debug("Entering checkChar(" + id + ")");
		
		char lastChar;
		char checkChar;
		int sum = 0;
		int value;
		char c;
		String key;
	
		if (id == null)
			return null;

		lastChar = id.charAt(id.length() - 1);
		for (int i = 0; i < id.length(); i++) {
			c = id.charAt(i);
			key = String.valueOf(c);
			if (ord.containsKey(key)) {
				value = ((Integer)ord.get(key)).intValue();
			} else {
				value = 0;
			}
			sum += (i + 1) * value;
		}
		checkChar = alpha[sum % alphaCount];
		if (lastChar == '+' || lastChar == checkChar) {
			return id + checkChar;
		} else {
			return null;
		}
	}


	/**
	 * @param id 
	 * @param verbose 
	 */
	void clearBindings(String id, boolean verbose) {
	
		Database db;
		Cursor cursor = null;
		
		try {
			db = noidDB.getDB();
			cursor = db.openCursor(null, null);
			String first = id + "\t";
			boolean skip = false;
			boolean done = false;

			String searchKey = first;
			String searchData = "0";
	
			DatabaseEntry theKey =
				new DatabaseEntry(searchKey.getBytes("UTF-8"));
			DatabaseEntry theData =
				new DatabaseEntry(searchData.getBytes("UTF-8"));
				
			// Perform the search
			OperationStatus status =
				cursor.getSearchKeyRange(theKey, theData, LockMode.DEFAULT);
				
			String foundKey = new String(theKey.getData());
			String foundData = new String(theData.getData());
	
			if (status == OperationStatus.NOTFOUND) {
				skip =  matches("^" + first + R + "/", foundKey);
				done = !matches("^" + first + R, foundKey);
			} else {
				done = true;
			}
	
			Pattern p = Pattern.compile("^[^\\t]*\\t(.*)");
			Matcher m = p.matcher("");
			StringBuffer buf = new StringBuffer();
			ArrayList<String> retVals = new ArrayList<String>();
			while (!done) {
				if (!skip && verbose) {
					m.reset(foundKey);
					buf.setLength(0);
					buf.append(m.find() ? m.group(1) : foundKey);
					buf.append(": clearing " + foundData.length() + " bytes");
					retVals.add(buf.toString());
					noidDB.remove(foundKey);
				}

				// Perform the search
				status = cursor.getNext(theKey, theData, LockMode.DEFAULT);
				
				foundKey = new String(theKey.getData());
				foundData = new String(theData.getData());
	
				if (status != OperationStatus.NOTFOUND
						|| !matches("^" + first, foundKey)) {
					done = true; // no more elements under id
				} else {
					skip = !matches("^" + first + R, foundKey);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeCursor(cursor);
		}
	}


	/**
	 * @param templ 
	 * @param term 
	 * @param naan 
	 * @param naa 
	 * @param subnaa 
	 */
	public String dbCreate(String templ, String term, String naan,
			String naa, String subnaa) {

		int total;
		String id;
		boolean success;
		boolean genOnly;
		String err;
		
		StringBuffer prefix = new StringBuffer();
		StringBuffer mask = new StringBuffer();
		StringBuffer genType = new StringBuffer();
		StringBuffer msg = new StringBuffer();

		String dbDirPath = dbHome + "/dbnoid";
		File dbDir = new File(dbDirPath);

		String dbFilePath = dbDirPath + "/noid.bdb";
		File dbFile = new File(dbFilePath);

		if (dbFile.exists()) {
			err = "error: a dbnoid database already exists in "
				+ dbDirPath
				+ ".\tTo permit creation of a new minter, rename\n"
				+ "\tor remove the entire dbnoid subdirectory.";
			addMsg(err);
			return null;
		}
		
		if (!dbDir.exists()) {
			debug("creating directory " + dbDirPath + " ...");
			try {
				dbDir.mkdir();
			} catch (Exception e) {
				addMsg("error: couldn't create database directory\n"
					+ dbDirPath + ": " + e.toString() + "\n");
				return null;
			}
		}

		if (templ == null) {
			genOnly = false;
			templ = ".zd";
		} else {
			genOnly = true;
		}

		debug("About to parse template.");
		total = parseTemplate(templ, prefix, mask, genType, msg);
		debug("TOTAL = " + total);

		if (total == 0) {
			addMsg(msg.toString());
			return null;
		}

		String synonym = "noid" + (genOnly ? "_" + msg : "any");

		String pattern = "\\S";
		Pattern p = Pattern.compile(pattern);

		if (contact == null || !p.matcher(contact).find()) {
			addMsg("error: contact (" + contact + ") must be non-empty");
			return null;
		}

		if (term == null) {
			term = "-";
		}

		boolean isTermLong = term.equals("long");
		
		if (!isTermLong
				&& !term.equals("medium")
				&& !term.equals("short")
				&& !term.equals("-")) {
			addMsg("error: term (" + term + ") must be either");
			return null;
		}

		if (naa == null)
			naa = "";
		if (naan == null)
			naan = "";
		if (subnaa == null)
			subnaa = "";

		if (term.equals("long")) {
		
			if (!p.matcher(naa).find()
					|| !p.matcher(naan).find()
					|| !p.matcher(subnaa).find()) {
				addMsg("error: longterm identifiers require "
					+ "an NAA Number, NAA, and SubNAA.");
			}

			if (!matches("\\d\\d\\d\\d\\d", naan)) {
				addMsg("error: term of \"long\" requires a "
					+ "5-digit NAAN (00000 if none), and non-empty "
					+ "string values for NAA and SubNAA.");
			}
			
		}

		if (!Util.storeFile(dbDirPath + "/log", ""))
			return null;
		if (!Util.storeFile(dbDirPath + "/logbdb", ""))
			return null;
	
		System.err.println("About to create database ...");
	
		Database myDatabase = null;
		
		
		noidDB = new NoidDB(dbHome + "/dbnoid");
		noidDB.open();

		if (myDatabase == null) {
			System.err.println(myDatabase);
			return null;
		}


		noidDB.rSet("naa", naa);
		noidDB.rSet("naan", naan);
		noidDB.rSet("subnaa", Util.unNullify(subnaa));
		
		noidDB.rSet("longterm", term.equals("long"));
		noidDB.rSet("wrap", term.equals("short"));
		
		noidDB.rSet("template", templ);
		System.err.println("Template = " + noidDB.rGet("template"));
		noidDB.rSet("prefix", prefix.toString());
		noidDB.rSet("mask", mask.toString());
		noidDB.rSet("firstpart", (!Util.isEmpty(naan) ? naan + "/" : "") + prefix);
		noidDB.rSet("addcheckchar", matches("k$", mask.toString()));
		
		noidDB.rSet("generator_type", genType.toString());
		noidDB.rSet("genonly", genOnly);
		
		noidDB.rSet("total", total);
		noidDB.rSet("padwidth", (total == NOLIMIT ? 16 : 2) + mask.length());
		
		noidDB.rSet("oacounter", 0);
		noidDB.rSet("oatop", total);
		noidDB.rSet("held", 0);
		noidDB.rSet("queued", 0);

		noidDB.rSet("fseqnum", SEQNUM_MIN);  // see queue() and mint()
		noidDB.rSet("gseqnum", SEQNUM_MIN);  // see queue()
		noidDB.rSet("gseqnum_date", 0);      // see queue()

		noidDB.rSet("version", VERSION);

		String pre = prefix.toString();
		String msk = mask.toString();

		p = Pattern.compile("[a-z]", Pattern.CASE_INSENSITIVE);
		pre = p.matcher(pre).replaceAll("e");

		p = Pattern.compile("k");
		msk = p.matcher(msk).replaceAll("e");

		p = Pattern.compile("^ze");
		msk = p.matcher(msk).replaceFirst("zeeee");

		String properties = "";
		
		properties += !naan.equals("") && naan.equals("00000") ? "G" : "-";
		properties += genType.equals("random") ? "R" : "-";
		
		p = Pattern.compile("eee");
		String tmp = pre + msk.substring(1);
		properties += genOnly && !p.matcher(tmp).find() ? "A" : "-";
	
		boolean isLongTerm = term.equals("long");
	
		properties += isLongTerm ? "N" : "-";
		
		p = Pattern.compile("-");
		properties += genOnly && !p.matcher(prefix).find() ? "I" : "-"; 
		
		properties += noidDB.rGetBool("addcheckchar") ? "T" : "-";
		
		int flags = Pattern.CASE_INSENSITIVE;
		properties += genOnly && (
				matches("[aeiouy]", prefix.toString(), flags) ||
				matches("[^rszdek]", mask.toString())
			)
			? "-"
			: "E";

		noidDB.rSet("properties", properties);

		String host = Util.getHostName();	

		String cwd = Util.getCwd();
		p = Pattern.compile("^/");
		if (!p.matcher(dbDirPath).find())
			cwd += "/" + dbDirPath;

		if (naa != null)
			naa = "no Name Assigning Authority";
		if (subnaa == null)
			subnaa = "no sub authority";
		if (naa == null)
			naan = "no NAA Number";

		debug("properties.length() = " + properties.length());
		p = Pattern.compile("-");
		String[] prop = new String[properties.length()];
		for (int i = 0; i < properties.length(); i++) {
			prop[i] = properties.charAt(i) == '-' ? "_ not" : "_____";
		}

		Random rand = new Random();
		String sample1, sample2;
		Integer randomSample1 = null;
		Integer randomSample2 = null;

		if (total == NOLIMIT) {
			debug("Generating random samples ...");
			randomSample1 = new Integer(rand.nextInt(10));
			randomSample2 = new Integer(rand.nextInt(100000));
		}

		sample1 = sample(randomSample1);
		sample2 = sample(randomSample2);

		String htotal = total == NOLIMIT ? "unlimited" : humanNum(total);
		String what = (total == NOLIMIT ? "unlimited" : total)
			+ " " + genType + " identifiers of form template\n"
			+ "       A Noid minting and binding database has "
			+ "been created that will bind\n       "
			+ (genOnly ? "" : "any identifier ") + "and mint "
			+ (total == NOLIMIT
				? "an unbounded number of identifiers\n"
					+ "       with the template \"" + templ + "\"."
				: htotal + " identifiers with the template \""
					+ templ + "\".")
			+ "\n"
			+ "       Sample identifiers would be \""
			+ sample1
			+ "\" and \""
			+ sample2
			+ "\".\n"
			+ "       Minting order is " + genType + ".";


		String buf = ""
			+ "# Creation record for the identifier generator in dbnoid/noid.bdb.\n"
			+ "#\n"
			+ "erc:\n"
			+ "who:       " + contact + "\n"
			+ "what:      " + what + "\n"
			+ "when:      " + temper() + "\n"
			+ "where:     " + host + ":" + cwd + "\n"
			+ "Version:   Noid " + VERSION + "\n"
			+ "Size:      " + (total == NOLIMIT ? "unlimited" : total) + "\n"
			+ "Template:  " + (Util.isEmpty(templ) ? "(:none)\n" : templ + "\n"
			+ "       A suggested parent directory for this template is \"" + synonym + "\".  Note:\n"
			+ "       separate minters need separate directories, and templates can suggest\n"
			+ "       short names; e.g., the template \"xz.redek\" suggests the parent directory\n"
			+ "       \"noid_xz4\" since identifiers are \"xz\" followed by 4 characters.\n")
			+ "Policy:    (:" + properties + ")\n"
			+ "       This minter's durability summary is (maximum possible being \"GRANITE\")\n"
			+ "         \"" + properties + "\", which breaks down, property by property, as follows.\n"
			+ "          ^^^^^^^\n"
			+ "          |||||||_" + prop[6] + " (E)lided of vowels to avoid creating words by accident\n"
			+ "          ||||||_" + prop[5] + " (T)ranscription safe due to a generated check character\n"
			+ "          |||||_" + prop[4] + " (I)mpression safe from ignorable typesetter-added hyphens\n"
			+ "          ||||_" + prop[3] + " (N)on-reassignable in life of Name Assigning Authority\n"
			+ "          |||_" + prop[2] + " (A)lphabetic-run-limited to pairs to avoid acronyms\n"
			+ "          ||_" + prop[1] + " (R)andomly sequenced to avoid series semantics\n"
			+ "          |_" + prop[0] + " (G)lobally unique within a registered namespace (currently\n"
			+ "                     tests only ARK namespaces; apply for one at ark@cdlib.org\n"
			+ "Authority: " + naa + " | " + subnaa + "\n"
			+ "NAAN:      " + naan + "\n";

		noidDB.rSet("erc", buf);

		if (!Util.storeFile(dbDirPath + "/README", buf))
			return null;

		// yyy useful for quick info on a minter from just doing 'ls dbnoid'??
		//          storefile("$dir/T=$prefix.$mask", "foo\n");

		String report = "Created:   minter for " + what + "  "
			+ "See " + dbDirPath + "/README for details.\n";


		if (Util.isEmpty(templ)) {
			noidDB.close();
			return report;
		}

		initCounters();
		noidDB.close();
		return report;
	}

	public int dbLock() {
		return 1;
	}

	public int dbUnlock() {
		return 1;
	}

	public boolean open(boolean create) {
		return open(create);
	}

	public boolean open() {
		return noidDB.open(false);
	}


	/**
	 * @param sleepValue 
	 */
	public void setLockTest (int sleepValue) {
		lockTest = sleepValue;
	}

    /**
     * @param noid 
     */
    public void dbClose() {
		noidDB.close();
	}

	public int dbInfo(String level) {
		return noidDB.dbInfo(level);
	}

	/**
	 * @param foundKey 
	 * @param foundData 
	 */
	int eachNoid (String foundKey, String foundData) {
		Cursor cursor = null;
		try {

			OperationStatus retVal;
			
			cursor = noidDB.getDB().openCursor(null, null);
		
			DatabaseEntry theKey = new DatabaseEntry();
			DatabaseEntry theData = new DatabaseEntry();

			if (foundKey == null)
				retVal = cursor.getNext(theKey, theData, LockMode.DEFAULT);
			else
				retVal = cursor.getFirst(theKey, theData, LockMode.DEFAULT);
			
			if (retVal == OperationStatus.SUCCESS) {
				foundKey  = new String(theKey.getData());
				foundData = new String(theData.getData());
				return 1;
			} else {
				return 0;
			}

		} catch (DatabaseException de) {
			// Exception handling goes here
		} finally {
			// Make sure to close the cursor
			closeCursor(cursor);
		}
		return 1;
	}

	/**
	 * @param buf 
	 */
	String echo (String buf) {
		debug("entering echo(" + buf + ")");
		return buf;
	}


	/**
	 * @param verbose 
	 * @param id 
	 * @param elems 
	 */
	public String fetch(boolean verbose, String id, String[] elems) {
		System.err.println("entering fetch()");
		String hdr = "";
		String retVal = "";

		if (id == null) {
			addMsg("error: "
				+ (verbose ? "fetch" : "get")
				+ " requires that an identifier be specified.");
			return null;
		}

		if (verbose) {
			String foo = id + "\t" + R + "/";
			String[] ids = { id };
			hdr = "id:    " + id
				+ (noidDB.exists(foo + "h") ? " hold" : "") + "\n"
				+ (!Util.isEmpty(validate("-", ids)) ? "" : getMsg() + "\n")
				+ "Circ:  "
				+ (!noidDB.isEmpty(foo + "c") ? noidDB.get(foo + "c") : "uncirculated")
				+ "\n";
		}

		Cursor cursor = null;

		try {
			cursor = noidDB.getDB().openCursor(null, null);

			if (elems.length == 0) {//No elements were specified, so find them.
				String first = "id" + "\t";
				boolean skip = false;
				boolean done = false;
				
				String searchKey = first;
				String searchData = "0";
	
				DatabaseEntry theKey =
					new DatabaseEntry(searchKey.getBytes("UTF-8"));
				DatabaseEntry theData =
					new DatabaseEntry(searchData.getBytes("UTF-8"));
				
				// Perform the search
				OperationStatus status =
					cursor.getSearchKeyRange(theKey, theData,
						LockMode.DEFAULT);
				
				String foundKey = new String(theKey.getData());
				String foundData = new String(theData.getData());
	
				if (status == OperationStatus.NOTFOUND) {
					skip =  matches("^" + first + R + "/", foundKey);
					done = !matches("^" + first + R, foundKey);
				} else {
					done = true;
				}
	
				while (!done) {
				
					if (!skip) {
						// if $verbose (ie, fetch), include label and
						// remember to strip "Id\t" from front of $key
						Pattern p = Pattern.compile("^[^\\t]*\\t(.*)");
						Matcher m = p.matcher(foundKey);
						
						retVal +=
							( verbose
								? ( m.find() ? m.group(1) : foundKey ) + ": "
								: "" )
							+ "$value\n";
					}
	
					// Perform the search
					status =
						cursor.getNext(theKey, theData, LockMode.DEFAULT);
				
					foundKey = new String(theKey.getData());
					foundData = new String(theData.getData());
	
					if (status != OperationStatus.NOTFOUND) {
						done = true; // no more elements under id
					} else {
						skip = !matches("^" + first + R, foundKey);
					}
				}
	
				closeCursor(cursor);
	
				if (retVal.length() == 0
						|| !matches("^" + first, foundKey)) {
					addMsg(hdr
						+ "note: no elements bound under " + id + ".");
					return null;
				}
				return hdr + retVal;
			}
		
			// yyy should this work for elem names with regexprs in them?
			// XXX idmap won't bind with longterm ???
			String idmapped;
			for (int i = 0; i < elems.length; i++) {
				String elem = elems[i];
				String key = id + "\t" + elem;
	
				if (noidDB.exists(key)) {
					if (verbose)
						retVal += elem = ": ";
					retVal += noidDB.get(key) + "\n";
				} else {
					idmapped = id2elemval(cursor, verbose, id, elem);
					if (verbose) {
						if (!Util.isEmpty(idmapped)) {
							retVal += idmapped + "\nnote: previous "
								+ "result produced by :idmap\n";
						} else {
							retVal += "error: \""
								+ id
								+ " "
								+ elem
								+ "\" is not bound.\n";
						}
					} else {
						retVal += idmapped + "\n";
					}
				}
			}
			closeCursor(cursor);
			return hdr + retVal;

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeCursor(cursor);
		}
		return hdr + retVal;
	}

	/**
	 * @param noid 
	 */
	public String genId() {
		System.err.println("entering genId()");
		dbLock();

		// Variables:
		//   oacounter	overall counter's current value (last value minted)
		//   oatop	overall counter's greatest possible value of counter
		//   saclist	(sub) active counters list
		//   siclist	(sub) inactive counters list
		//   c$n/value	subcounter name's ($scn) value

		int oaCounter = noidDB.rGetInt("oacounter");
	
		// yyy what are we going to do with counters for held? queued?
	
		if (noidDB.rGetInt("oatop") != NOLIMIT
				&& oaCounter >= noidDB.rGetInt("oatop")) {
	
			// Critical test of whether we're willing to re-use
			// identifiers by re-setting (wrapping) the counter to
			// zero.  To be extra careful we check both the longterm
			// and wrap settings, even though, in theory, wrap won't
			// be set if longterm is set.

			if (noidDB.rGetBool("longterm") || !noidDB.rGetBool("wrap")) {
				dbUnlock();
				String msg = "error: identifiers exhausted (stopped at "
					+ noidDB.rGet("oatop") + ").";
				noidDB.addMsg(msg);
				noidDB.writeLog(msg);
				return null;
			}

			// If we get here, term is not "long".
			noidDB.writeLog(temper() + ": Resetting counter to zero; "
				+ "previously issued identifiers will be re-issued");

			System.err.println("Checking if we neet to init");
			if (noidDB.rGet("generator_type").equals("sequential")) {
				noidDB.rSet("oacounter", 0);
			} else {
				initCounters();	// yyy calls dblock -- problem?
			}

			oaCounter = 0;
		} else {

		}
		
		// If we get here, the counter may actually have just been reset.
		// Deal with the easy sequential generator case and exit early.
		// 
		if (noidDB.rGet("generator_type").equals("sequential")) {
			String id = n2xdig(noidDB.rGetInt("oacounter"), noidDB.rGet("mask"));

			// incr to reflect new total	
			noidDB.rInc("oacounter");
			dbUnlock();
			return id;
		}

		// If we get here, the generator must be of type "random".

		StringTokenizer st = new StringTokenizer(noidDB.rGet("saclist"));
		int len = st.countTokens();

		if (len < 1) {
			dbUnlock();
			noidDB.addMsg("error: no active counters panic, "
				+ "but $oacounter identifiers left?");
			return null;
		}

		String[] sacList = new String[len];
		for (int i = 0; i < len; i++)
			sacList[i] = st.nextToken();


		Random rand = new Random();

		int randn = rand.nextInt(len);	// pick a specific counter name
		String sctrn = sacList[randn];	// at random; then pull its $n
		
		int n = Integer.parseInt(sctrn.substring(1));	// numeric equivalent from the name
		//print "randn=$randn, sctrn=$sctrn, counter n=$n\t";

		int sctr = noidDB.rGetInt(sctrn + "/value");	// and get its value
		sctr++;				// increment and
		noidDB.rSet(sctrn + "/value", sctr);	// store new current value
		noidDB.rInc("oacounter");		// incr overall counter - some
							// redundancy for sanity's sake
	
		// deal with an exhausted subcounter
		if (sctr >= noidDB.rGetInt(sctrn + "/top")) {
			String modSacList = "";

			for (int i = 0; i < sacList.length; i++) {
				if (sacList[i].equals(sctrn))
					continue;
				modSacList += sacList[i] + " ";
			}
			
			noidDB.set("saclist", modSacList);		// update saclist
			noidDB.append("siclist", " " + sctrn);		// and siclist
			//print "===> Exhausted counter $sctrn\n";
		}
	
		// $sctr holds counter value, $n holds ordinal of the counter itself
		String id = n2xdig(
				sctr + (n * noidDB.rGetInt("percounter")),
				noidDB.rGet("mask"));
		dbUnlock();
		return id;
	}


	/**
	 * @param noid 
	 * @param id 
	 */
	public String getCircSvec (String id) {

		String circRec = noidDB.get(id + "\t" + R + "/c");

		if (circRec == null)
			return "";
	
		// Circulation status vector (string of letter codes) is the 1st
		// element, elements being separated by '|'.  We don't care about
		// the other elements for now because we can find everything we
		// need at the beginning of the string (without splitting it).
		// Let errors hit the log file rather than bothering the caller.
	
		StringTokenizer st = new StringTokenizer(circRec, "|");
		int numTokens = st.countTokens();

		String circSvec = null;
		if (st.hasMoreElements())
			circSvec = st.nextToken();
	
		if (circSvec == null || circSvec.length() == 0) {
			logMsg("error: id " + id + " has no circ status vector -- "
				+ "circ record is " + circRec);
			return "";
		}

		Pattern p = Pattern.compile("^([iqu])[iqu]*$");
		Matcher m = p.matcher(circSvec);
			
		if (!m.find()) {
			logMsg("error: id $id has a circ status vector "
				+ "containing letters other than 'i', "
				+ "'q', or 'u' -- circ record is " + circRec);
			return "";
		}
		
		return m.group(1);

	}

	/**
	 * @param noid 
	 * @param id 
	 * @param circSvec 
	 * @param date 
	 * @param contact 
	 */
	public String setCircRec(String id, String circSvec,
			String date, String contact)  {
	
		System.err.println("Entering setCircRec id = " + id);

		boolean status = true;
		String circRec = circSvec
			+ "|" + date
			+ "|" + contact
			+ "|" + noidDB.rGet("oacounter");
	
		// yyy do we care what the previous circ record was?  since right now
		//     we just clobber without looking at it
	
		dbLock();
	
		// Check for and clear any bindings if we're issuing an identifier.
		// We ignore the return value from clear_bindings().
		// Replace or clear admin bindings by hand, including pepper if any.
		// 		yyy pepper not implemented yet
		// If issuing a longterm id, we automatically place a hold on it.
		
		if (matches("^i", circSvec)) {
			clearBindings(id, false);
			noidDB.remove(id + "\t" + R + "/p");

			if (noidDB.rGetBool("longterm")) {
				status = holdSet(id);
			}
		}
		
		noidDB.set(id + "\t" + R + "/c", circRec);
	
		dbUnlock();
	
		// This next logMsg should account for the bulk of the log when
		// longterm identifiers are in effect.
		//
		if (noidDB.rGetBool("longterm"))
			logMsg("m: " + circRec
				+ (status ? "" : " -- hold failed"));
	
		if (!status)			// must be an error in hold_set()
			return null;
		return id;

	}

	/**
	 * @param noid 
	 * @param varName 
	 */
	String getNoid(String varName) {
		return noidDB.get(R + "/" + varName);
	}

	/**
	 * @param action
	 * @param id 
	 */
	public boolean hold(String action, String id) {
		String[] ids = { id };
		return hold(action, ids);
	}

	/**
	 * @param action 
	 * @param ids 
	 */
	public boolean hold(String action, String[] ids) {

		if (contact == null) {
			addMsg("error: contact undefined");
			return false;
		}

		if (action == null) {
			addMsg("error: hold \"set\" or \"release\"?");
			return false;
		}

		if (ids == null || ids.length == 0) {
			addMsg("error: no Id(s) specified");
			return false;
		}

		if (!action.equals("set") && !action.equals("release")) {
			addMsg("error: unrecognized hold directive (" + action + ")");
			return false;
		}

		boolean release = action.equals("release");
		
		// yyy what is sensible thing to do if no ids are present?

		// XXX Bug in Noid.pm
		String iderror = "";
		if (noidDB.rGetBool("genonly")) {
			iderror = Util.join("", validate("-", ids));
			if (!matches("error:", iderror)) {
				iderror = "";
			}
		}

		if (!Util.isEmpty(iderror)) {
			addMsg("error: hold operation not started -- one or "
				+ "more ids did not validate:\n" + iderror);
			return false;
		}
	
		boolean status;
		int n = 0;
	
		for (int i = 0; i < ids.length; i++) {
			String id = ids[i];
			
			if (release) {		// no hold means key doesn't exist
				if (noidDB.rGetBool("longterm")) {
					logMsg(temper() + " " + id + ": releasing hold");
				}
				dbLock();
				status = holdRelease(id);
			} else {			// "hold" means key exists
				if (noidDB.rGetBool("longterm")) {
					logMsg(temper() + " " + id + ": placing hold");
				}
				dbLock();
				status = holdSet(id);
			}
			dbUnlock();
			if (!status)
				return false;
			n++;			// xxx should report number

			// Incr/Decrement for each id rather than by scalar(@ids);
			// if something goes wrong in the loop, we won't be way off.

			// XXX should we refuse to hold if "long" and issued?
			//     else we cannot use "hold" in the sense of either
			//     "reserved for future use" or "reserved, never issued"
			//
		}
	
		addMsg("ok: " + n + " hold" + (n == 1 ? "" : "s") + " placed");
		return true;
	}

	/**
	 * @param noid 
	 * @param id 
	 */
	boolean holdSet(String id) {
		noidDB.set(id + "\t" + R + "/h", 1);	// value doesn't matter
		noidDB.rInc("held");
		if (noidDB.rGetInt("total") != NOLIMIT	// ie, if total is non-zero
				&& noidDB.rGetInt("held") > noidDB.rGetInt("oatop")) {
			String m = "error: hold count (" + noidDB.rGet("held")
				+ ") exceeding total possible on id " + id;
			addMsg(m);
			logMsg(m);
			return false;
		}
		return true;
	}

	/**
	 * Place or release hold.
	 * 
	 * @param noid 
	 * @param id 
	 */
	boolean holdRelease (String id) {
		
		noidDB.remove(id + "\t" + R + "/h");
		noidDB.rDec("held");

		if (noidDB.rGetInt("held") < 0) {
			String m = "error: hold count (" + noidDB.rGet("held") 
				+ ") going negative on id " + id;
			addMsg(m);
			logMsg(m);
			return false;
		}
		return true;
	}

	/**
	 * @param num 
	 */
	public String humanNum (int num) {
		DecimalFormat df = new DecimalFormat("#,###");
		return df.format(num);
	}


	/**
	 * @param cursor 
	 * @param verbose 
	 * @param id 
	 * @param elem 
	 */
	String id2elemval (Cursor cursor, boolean verbose,
			String id, String elem) {

		String first = R + "/idmap/" + elem + "\t";

		String searchKey = first;
		String searchData = "0";

		try {
		
			DatabaseEntry theKey =
		 		new DatabaseEntry(searchKey.getBytes("UTF-8"));
			DatabaseEntry theData =
		 		new DatabaseEntry(searchData.getBytes("UTF-8"));
		
			OperationStatus status =
				cursor.getSearchKeyRange(theKey, theData, LockMode.DEFAULT);
		
			if (status == OperationStatus.NOTFOUND) {
				return "error: id2elemval: c_get status/errno ("
					+ status + "/" + ")";
			}
				
			String foundKey = new String(theKey.getData());
			String foundData = new String(theData.getData());
	
			if (!matches("^" + first, foundKey))
				return "";
	
			Pattern p = Pattern.compile(first + "(.+)");
			Matcher m = p.matcher("");
			String patt;
			String newVal;
			while (true) { // exhaustively visit all patterns for this element
				patt = null;
				m.reset(foundKey);
				if (m.find())
					patt = m.group(1);
				newVal = id;
				
				if (patt != null) {
					// yyy kludgy use of unlikely delimiters
					Pattern p1 = Pattern.compile(patt);
					Matcher m1 = p1.matcher(newVal);
					newVal = m1.replaceFirst(foundData);
					// XXX what is start if it doesnt match
					if (m1.start() >= 0) {
						return (verbose ? elem + ": " : "") + newVal;
					}
				}
		
				status = cursor.getSearchKeyRange(theKey, theData,
									LockMode.DEFAULT);
				
				if (status == OperationStatus.NOTFOUND)
					return "";
				foundKey = new String(theKey.getData());
				foundData = new String(theData.getData());
				if (!matches("^" + first, foundKey))
					return "";
			}

		} catch (Exception e) {

		} finally {
			closeCursor(cursor);
		}
		return "";
	}


	/**
	 * @param noid 
	 */
	void initCounters() {
		// Variables:
		//   oacounter	overall counter's current value (last value minted)
		//   saclist	(sub) active counters list
		//   siclist	(sub) inactive counters list
		//   c$n/value	subcounter name's ($n) value
		//   c$n/top	subcounter name's greatest possible value
	
		dbLock();

		noidDB.rSet("oacounter", 0);
		int total = noidDB.rGetInt("total");

		int maxCounters = 293;		// prime, a little more than 29*10

		// Using a prime under the theory (unverified) that it may
		// help even out distribution across the more significant
		// digits of generated identifiers.  In this way, for
		// example, a method for mapping an identifier to a pathname
		// (eg, fk9tmb35x -> fk/9t/mb/35/x/, which could be a
		// directory holding all files related to the named object),
		// would result in a reasonably balanced filesystem tree --
		// no subdirectories too unevenly loaded.  That's the hope
		// anyway.

		// max per counter, last has fewer round up to be > 0
		noidDB.rSet("percounter", total / maxCounters + 1);

		int n = 0;
		int t = total;
		int pctr = noidDB.rGetInt("percounter");
		String saclist = "";
		String cn;
		
		while (t > 0) {
			cn = "c" + n;
			
			noidDB.rSet(cn + "/top", (t >= pctr ? pctr : t));
			noidDB.rSet(cn + "/value", 0);		// yyy or 1?

			saclist += "c" + n + " ";
			t -= pctr;
			n++;
		}

		System.err.println("SACLIST = " + saclist);
		noidDB.rSet("saclist", saclist);
		noidDB.rSet("siclist", "");
		n--;

		dbUnlock();
	
	}

	public String getContact() {
		return contact;
	}

	/**
	 * @param pepper 
	 */
	public String mint(boolean pepper) {

		if (contact == null) {
			addMsg("contact undefined");
			return null;
		}

		if (Util.isEmpty(noidDB.rGet("template"))) {
			addMsg("error: this minter does not generate "
				+ "identifiers (it does accept user-defined "
				+ "identifier and element bindings).");
			return null;
		}

	
		// Check if the head of the queue is ripe.  See comments under queue()
		// for an explanation of how the queue works.
		String currdate = temper();		// fyi, 14 digits long
		String first = R + "/q/";
		Database db = noidDB.getDB();

		Cursor cursor = null;

		try {
		
			cursor = db.openCursor(null, null);
		
			if (cursor == null) {
				addMsg("couldn't create cursor");
			}
	
			// The following is not a proper loop.  Normally it
			// should run once, but several cycles may be needed to
			// weed out anomalies with the id at the head of the
			// queue.  If all goes well and we found something to
			// mint from the queue, the last line in the loop exits
			// the routine.  If we drop out of the loop, it's
			// because the queue wasn't ripe.

			String id = "";
			OperationStatus status = null;
			String searchKey;
			String searchData;
			String qdate;
			String circSvec;
			
			while (true) {
				searchKey = first;
				searchData = id;
			
				// System.err.println("Searching for " + searchKey);
				DatabaseEntry theKey =
			 		new DatabaseEntry(searchKey.getBytes("UTF-8"));
				DatabaseEntry theData =
			 		new DatabaseEntry(searchData.getBytes("UTF-8"));
				// System.err.println("finished searching");
				
				status =
					cursor.getSearchKeyRange(theKey, theData, LockMode.DEFAULT);

				System.err.println("status = " + status);

				if (status == OperationStatus.NOTFOUND) {
					addMsg("mint: c_get status/errno ("
						+ status + "/" + ")");
					System.err.println("Returning");
					return null;
				}
				
				String foundKey = new String(theKey.getData());
				String foundData = new String(theData.getData());
				// System.err.println("Found data " + foundData);
				
				// The cursor, key and value are now set at the first item
				// whose key is greater than or equal to $first.  If the
				// queue was empty, there should be no items under "$R/q/".
				qdate = null;
				Pattern p = Pattern.compile(R + "/q/(\\d{14})");
				Matcher m = p.matcher(foundKey);
				if (m.find()) {
					qdate = m.group(1); 
				}
			
				if (qdate == null) {			// nothing in queue
					// this is our chance -- see queue() comments for why
					if (noidDB.rGetInt("fseqnum") > SEQNUM_MIN) {
						noidDB.rSet("fseqnum", SEQNUM_MIN);
					}
					break;  // so move on
				}
						
				// If the date of the earliest item to re-use hasn't arrived
				if (currdate.compareTo(qdate) < 0) {
					break;				// move on
				}
	
				// If we get here, head of queue is ripe.  Remove from queue.
				// Any "next" statement from now on in this loop discards the
				// queue element.
				// XXX Convert to Java
				//db.delete(null, theKey);
				cursor.delete();
	
				int queued = noidDB.rGetInt("queued");
				System.err.println("queued = " + queued);
				noidDB.rDec("queued");
			
				if (queued <= 0) {
					String msg = "error: queued count (" + noidDB.rGet("queued")
						+ ") going negative on id " + id;
					addMsg(msg);
					logMsg(msg);
					return null;
				}
	
				// We perform a few checks first to see if we're actually
				// going to use this identifier.  First, if there's a hold,
				// remove it from the queue and check the queue again.
				
				if (noidDB.exists(id + "\t" + R + "/h")) {	// if there's a hold
					if (noidDB.rGetBool("longterm"))
						logMsg("warning: id "
							+ id + " found in queue with a hold placed on "
							+ "it -- removed from queue.");
					continue;
				}
			
				// yyy this means id on "hold" can still have a 'q' circ status?
				circSvec = getCircSvec(id);
	
				if (matches("^i", circSvec)) {
					logMsg("error: id " + id + " appears to have been "
						+ "issued while still in the queue -- "
						+ "circ record is " +  noidDB.get(id + "\t" + R + "/c"));
					continue;
				}
	
				if (matches("^u", circSvec)) {
					logMsg("note: id " + id + ", marked as unqueued, is "
						+ "now being removed/skipped in the queue -- "
						+ "circ record is " + noidDB.get(id + "\t" + R + "/c"));
					continue;
				}
	
				StringBuffer buf = new StringBuffer();
				if (matches("^([^q])", circSvec, 1, buf)) { 
					logMsg("error: id " + id + " found in queue has an "
						+ "unknown circ status (" + buf + ") -- "
						+ "circ record is " + noidDB.get(id + "\t" + R + "/c"));
				}
	
				// Finally, if there's no circulation record, it means that
				// it was queued to get it minted earlier or later than it
				// would normally be minted.  Log if term is "long".
				if (circSvec.length() == 0) {
					if (noidDB.rGetBool("longterm")) {
						logMsg("note: "
							+ "queued id " + id + " coming out of queue on first "
							+ "minting (pre-cycled)");
					}
				}
	
				// If we get here, our identifier has now passed its tests.
				// Do final identifier signoff and return.
				System.err.println("Mint: About to enter setCircRec()");
				return setCircRec(id, "i" + circSvec, currdate, contact);
			}
	
			// If we get here, we're not getting an id from the queue.
			// Instead we have to generate one.
			//
			// As above, the following is not a proper loop.  Normally it should
			// run once, but several cycles may be needed to weed out anomalies
			// with the generated id (eg, there's a hold on the id, or it was
			// queued to delay issue).
			// 
			while (true) {
	
				// Next is the important seeding of random number generator.
				// We need this so that we get the same exact series of
				// pseudo-random numbers, just in case we have to wipe out a
				// generator and start over.  That way, the n-th identifier
				// will be the same, no matter how often we have to start
				// over.  This step has no effect when $generator_type ==
				// "sequential".
				//
				// XXX do this in java
				//srand(noidDB.rGetInt("oacounter"));
	
				// The id returned in this next step may have a "+" character
				// that n2xdig() appended to it.  The checkchar() routine
				// will convert it to a check character.
				//
				System.err.println("About to generate ID ...");
				id = genId();
				if (id == null)
					return null;
				System.err.println("ID = " + id);
	
				// Prepend NAAN and separator if there is a NAAN.
				//
				if (!noidDB.rIsEmpty("firstpart"))
					id = noidDB.rGet("firstpart") + id;
	
				// Add check character if called for.
				System.err.println("addcheckvar = " + noidDB.rGet("addcheckchar"));
				if (noidDB.rGetBool("addcheckchar"))
					id = checkChar(id);
				
				System.err.println("ID = " + id);
	
				// There may be a hold on an id, meaning that it is not to
				// be issued (or re-issued).
				//
				if (noidDB.exists(id + "\t" + R + "/h"))		// if there's a hold
					continue;				// do genid() again
	
				// It's usual to find no circulation record.  However,
				// there may be a circulation record if the generator term
				// is not "long" and we've wrapped (restarted) the counter,
				// of if it was queued before first minting.  If the term
				// is "long", the generated id automatically gets a hold.
				//
				circSvec = getCircSvec(id);
	
				// A little unusual is the case when something has a
				// circulation status of 'q', meaning it has been queued
				// before first issue, presumably to get it minted earlier or
				// later than it would normally be minted; if the id we just
				// generated is marked as being in the queue (clearly not at
				// the head of the queue, or we would have seen it in the
				// previous while loop), we go to generate another id.  If
				// term is "long", log that we skipped this one.
				//
				if (matches("^q", circSvec)) {
					if (noidDB.rGetBool("longterm")) {
						logMsg("note: will not issue genid()'d "
							+ id + " as it's "
							+ "status is 'q', circ_rec is "
							+ noidDB.get(id + "\t" + R + "/c"));
					}
					continue;
				}
	
				// If the circulation status is 'i' it means that the id is
				// being re-issued.  This shouldn't happen unless the counter
				// has wrapped around to the beginning.  If term is "long",
				// an id can be re-issued only if (a) its hold was released
				// and (b) it was placed in the queue (thus marked with 'q').
				//
				if (matches("^i", circSvec) &&
						(noidDB.rGetBool("longterm") ||
						!noidDB.rGetBool("wrap"))) {		
					logMsg("error: id " + id + " cannot be "
						+ "re-issued except by going through the "
						+ "queue, circ_rec " + noidDB.get(id + "\t" + R + "/c"));
					continue;
				}
				
				if (matches("^u", circSvec)) {
					logMsg("note: generating id " + id + ", currently "
						+ "marked as unqueued, circ record is "
						+ "queue, circ_rec " + noidDB.get(id + "\t" + R + "/c"));
					continue;
				}
	
				StringBuffer buf = new StringBuffer();
				if (matches("^([^iqu])", circSvec, 1, buf)) {
					logMsg("error: id " + id + " has unknown circulation "
						+ "status (" + buf + "), circ_rec "
						+ noidDB.get(id + "\t" + R + "/c"));
					continue;
				}
			
				//
				// Note that it's OK/normal if $circ_svec was an empty string.
	
				// If we get here, our identifier has now passed its tests.
				// Do final identifier signoff and return.
				//
				System.err.println("About to enter setCircRec()");
				return setCircRec(id, 'i' + circSvec, currdate, contact);
			}
			// yyy
			// Note that we don't assign any value to the very important key=$id.
			// What should it be bound to?  Let's decide later.
	
			// yyy
			// Often we want to bind an id initially even if the object or record
			// it identifies is "in progress", as this gives way to begin tracking,
			// eg, back to the person responsible.
			
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeCursor(cursor);
		}
		return "";
	}

	/**
	 * Record user (":/:/...") values in admin area.
	 *
	 * @param key  the key for data to be stored
	 * @param data the data to be stored
	 * @return     <code>true</code> on success; <code>false</code>
	 *             otherwise
	 */
	boolean note(String key, String data) {

		Database db = noidDB.getDB();

		String aKey = R + "/" + R + "/" + key;
		String aData = data;

		OperationStatus status = null;
		boolean isSuccess = false;

		try {
			DatabaseEntry theKey =
				new DatabaseEntry(aKey.getBytes("UTF-8"));
			DatabaseEntry theData =
				new DatabaseEntry(aData.getBytes("UTF-8"));

			dbLock();
			status = db.put(null, theKey, theData);
			dbUnlock();

			isSuccess = status == OperationStatus.SUCCESS;

			if (noidDB.rGetBool("longterm")) {
				logMsg("note: note attempt under "
					+ key + " by " + contact
					+ (isSuccess ? "" : " -- note failed"));
			}

			if (!isSuccess) {
				addMsg("put status (" + status + ")");
			}

		} catch (DatabaseException e) {
			addMsg("put(" + aKey + "," + aData + ") " + " failed. " + e);
		} catch (UnsupportedEncodingException e) {
			System.err.println("UTF-8 charset not supported. " + e);
		}

		return isSuccess;
	}

	/**
	 * @param num 
	 * @param mask 
	 */
	public String n2xdig (int num, String mask) {
		String s = "";
		int varWidth = 0;
		int pos = mask.length() - 1;
		char c;
		int div = 0;
		int remainder;
	
		debug("entering PATTERN");
		Pattern p = Pattern.compile("^[rsz][de]+k?$");
		if (!p.matcher(mask).find())
			return null;

		debug("entering while loop ...");
		while (num != 0 || varWidth == 0) {
			if (varWidth == 0) {
				if (pos < 0)
					break;
				c = mask.charAt(pos--);
				if (c == 'r' || c == 's') {
					break;
				} else if (c == 'e') {
					div = alphaCount;
				} else if (c == 'd') {
					div = digitCount;
				} else if (c == 'z') {
					varWidth = 1;
					continue;
				} else if (c == 'k') {
					continue;
				}
			}
			remainder = num % div;
			num = num / div;
			s = alpha[remainder] + s;
		}

		p = Pattern.compile("k$");
		if (p.matcher(mask).find()) {
				s += "+";
		}
		return s;
	}

	/**
	 * @param templ 
	 * @param prefix 
	 * @param mask 
	 * @param genType 
	 * @param message 
	 */
	private int parseTemplate(String templ, StringBuffer prefix,
			StringBuffer mask,
			StringBuffer genType, StringBuffer message) {

		System.err.println("Entering parseTemplate()");

		String dirName = null;
		
		if (templ == null)
			templ = "";

		String patt = "[/\\s]+$";
		Pattern r = Pattern.compile(patt);
		Matcher m = r.matcher(templ);
		templ = m.replaceAll("");

		patt = "^(.*/)?([^/]+)$";
		r = Pattern.compile(patt);
		m = r.matcher(templ);
		if (m.find()) {
			dirName = m.group(1);
			templ = m.group(2);
		}
		
		if (dirName == null)
			dirName = "";

		if (templ == null || templ.equals("-")) {
			debug("No minting is possible.");
			Util.setBuffer(message, "parseTemplate: no minting possible.");
			Util.setBuffer(prefix, "");
			Util.setBuffer(mask, "");
			Util.setBuffer(genType, "");
			return NOLIMIT;
		}

		patt = "^([^\\.]*)\\.(\\w+)";
		r = Pattern.compile(patt);
		m = r.matcher(templ);
		if (!m.find()) {
			Util.setBuffer(message, "parseTemplate: no template mask - "
				+ "can't generate identifiers");
			return 0;
		}
		Util.setBuffer(prefix, Util.unNullify(m.group(1)));
		Util.setBuffer(mask, m.group(2));

		String maskStr = mask.toString();

		if (!matches("^[rsz]", maskStr)) {
			Util.setBuffer(message, "parseTemplate: mask must begin "
				+ "with one of "
				+ "the letters\n'r' (random), 's' (sequential), "
				+ "or 'z' (sequential unlimited).");
			return 0;
		}

		if (!matches("^.[^k]+k?$", maskStr)) {
			Util.setBuffer(message, "parseTemplate: exactly one "
				+ "check character "
				+ "(k) is allowed, and it may\nonly appear at the "
				+ "end of a string of one or more mask characters.");
			return 0;
		}

		if (!matches("^.[de]+k?$", maskStr)) {
			Util.setBuffer(message, "parseTemplate: a mask may contain "
				+ "only the letters 'd' or 'e'.");
			return 0;
		}

		if (matches("k$", maskStr)) {
			for (int i = 0; i < prefix.length(); i++) {
				char c = prefix.charAt(i);
				
				if (c != '/' && !ord.containsKey(String.valueOf(c))) {
					Util.setBuffer(message, "parseTemplate: with a "
						+ "check character "
						+ "at the end, a mask may contain only "
						+ "characters from \"" + legalString + "\".");
					return 0;
				}
			}
		}

		int maskLen = maskStr.length() - 1;
		Util.setBuffer(message, prefix + Integer.toString(maskLen));
		if (matches("^z", maskStr)) {
			message.append("+");
		}

		int total = 1;
		for (int i = 0; i < maskStr.length(); i++) {
			char c = maskStr.charAt(i);
			debug("char = " + c);
			if (c == 'e') {
				total *= alphaCount;
			} else if (c == 'd') {
				total *= digitCount;
			}
			debug("total = " + total);
		}

		if (matches("^r", maskStr)) {
			Util.setBuffer(genType, "random");
		} else {
			Util.setBuffer(genType, "sequential");
		}

		if (matches("^z", maskStr)) {
			return NOLIMIT;
		} else {
			return total;
		}

	}

	/**
	 * @param pattern 
	 * @param input 
	 */
	boolean matches(String pattern, String input) {
		return matches(pattern, input, 0);
	}

	/**
	 * @param pattern 
	 * @param input 
	 * @param flags 
	 */
	boolean matches(String pattern, String input, int flags) {
		return Pattern.compile(pattern, flags).matcher(input).find();
	}

	/**
	 * @param pattern 
	 * @param input 
	 * @param groupNum 
	 * @param buf 
	 */
	boolean matches(String pattern, String input,
			int groupNum, StringBuffer buf) {
		Pattern p = Pattern.compile(pattern);
		Matcher m = p.matcher(input);
		if (m.find()) {
			buf.setLength(0);
			buf.append(m.group(groupNum));
			return true;
		} else {
			return false;
		}
	}



	/**
	 * @param when 
	 * @param id 
	 */
	public String[] queue(String when, String id) {
		String[] ids = { id };
		return queue(when, ids);
	}


	/**
	 * @param when 
	 * @param ids 
	 */
	public String[] queue (String when, String[] ids) {
	
		String[] empty = new String[0];
	
		if (noidDB.rGetBool("template")) {
			addMsg("error: queuing makes no "
				+ "sense in a bind-only minter.");
			return empty;
		}
	
		if (contact == null) {
			addMsg("error: contact undefined");
			return empty;
		}
	
		if (when == null || !matches("\\S", when)) {
			addMsg("error: queue when? (eg, first, lvf, 30d, now)");
			return empty;
		}
	
		//yyy what is sensible thing to do if no ids are present?
		if (ids.length < 1) {
			addMsg("error: must specify at least one id to queue.");
			return empty;
		}
	
		int seqnum = 0;
		boolean del = false;
		
		String fixsqn;
		String qdate = null;			// purposely undefined
	
		// You can express a delay in days (d) or seconds (s, default).
		//
		
		long now = System.currentTimeMillis() / 1000;
		Pattern patt = Pattern.compile("^(\\d+)([ds]?)$");
		Matcher matcher = patt.matcher(when);
		
		if (matcher.find()) {	// current time plus a delay
			// The number of seconds in one day is 86400.
			String c = matcher.group(2);
			int multiplier = c != null && c.equals("d") ? 86400 : 1;
			qdate = temper(now +
				Long.parseLong(matcher.group(1)) * multiplier);
		}
		else if (when.equals("now")) {	// a synonym for current time
			qdate = temper(now);
		}
		else if (when.equals("first")) {
			// Lowest value first (lvf) requires $qdate of all zeroes.
			// To achieve "first" semantics, we use a $qdate of all
			// zeroes (default above), which means this key will be
			// selected even earlier than a key that became ripe in the
			// queue 85 days ago but wasn't selected because no one
			// minted anything in the last 85 days.
			//
			seqnum = noidDB.rGetInt("fseqnum");
			//
			// NOTE: fseqnum is reset only when queue is empty; see mint().
			// If queue never empties fseqnum will simply keep growing,
			// so we effectively truncate on the left to 6 digits with mod
			// arithmetic when we convert it to $fixsqn via sprintf().
		}
		else if (when.equals("delete")) {
			del = true;
		}
		else if (!when.equals("lvf")) {
			addMsg("error: unrecognized queue time: " + when);
			return empty;
		}
	
		if (qdate != null) {		// current time plus optional delay
			if (qdate.compareTo(noidDB.rGet("gseqnum_date")) > 0) {
				seqnum = SEQNUM_MIN;
				noidDB.rSet("gseqnum", SEQNUM_MIN);
				noidDB.rSet("gseqnum_date", qdate);
			} else {
				seqnum = noidDB.rGetInt("gseqnum");
			}
		} else { 
			qdate = "00000000000000";	// this needs to be 14 zeroes
		}
	
		String iderror = "";
		
		if (noidDB.rGetBool("genonly")) {
			// XXX Fix this scalar context biz
// 			iderror = validate("-", ids);
// 			if (!matches("error:", iderror)) {
// 				iderror = "";
// 			}
		}
		
		if (!Util.isEmpty(iderror)) {
			addMsg("error: queue operation not started -- one or "
				+ "more ids did not validate:\n" + iderror);
			return null;
		}
	
		
		String firstpart = noidDB.rGet("firstpart");
		int padwidth = noidDB.rGetInt("padwidth");
		String currdate = temper();
		
		ArrayList<String> retvals = new ArrayList<String>();
		String m;
		String idval;
		String paddedid;
		String circSvec;
		
		for (int i = 0; i < ids.length; i++) {
			String id = ids[i];
	
			if (noidDB.exists(id + "\t" + R + "/h")) {		// if there's a hold
				m = "error: a hold has been set for \"" + id + "\" and "
					+ "must be released before the identifier can "
					+ "be queued for minting.";
				logMsg(m);
				retvals.add(m);
				continue;
			}
	
			// If there's no circulation record, it means that it was
			// queued to get it minted earlier or later than it would
			// normally be minted.  Log if term is "long".
			//
			circSvec = getCircSvec(id);
	
			if (matches("^q", circSvec) && !del) {
				m = "error: id " + id + " cannot be queued since "
					+ "it appears to be in the queue already -- "
					+ "circ record is " + noidDB.rGet(id + "\t" + R + "/c");
				logMsg(m);
				retvals.add(m);
				continue;
			}
			
			if (matches("^u", circSvec) && del) {
				m = "error: id " + id + " has been unqueued already -- "
					+ "circ record is " + noidDB.rGet(id + "\t" + R + "/c"); 
				logMsg(m);
				retvals.add(m);
				continue;
			}
			
			if (!matches("^q", circSvec) && del) {
				m = "error: id " + id + " cannot be unqueued since its circ "
					+ "record does not indicate its being queued, "
					+ "circ record is " + noidDB.rGet(id + "\t" + R + "/c");
				logMsg(m);
				retvals.add(m);
				continue;
			}
			// If we get here and we're deleting, circ_svec must be 'q'.
	
			if (circSvec.equals("")) {
				if (noidDB.rGetBool("longterm"))
					logMsg("note: "
						+ "id " + id + " being queued before first "
						+ "minting (to be pre-cycled)");
			} else if (matches("^i", circSvec)) {
				if (noidDB.rGetBool("longterm"))
					logMsg("note: "
						+ "longterm id " + id + " being queued for re-issue");
			}
	
			// yyy ignore return OK?
			setCircRec(id, (del ? "u" : "q") + circSvec,
					currdate, contact);
	
			// XXX
			//($idval = $id) =~ s/^$firstpart//;
			//$paddedid = sprintf("%0$padwidth" . "s", $idval);
			//$fixsqn = sprintf("%06d", $seqnum % SEQNUM_MAX);
			
			patt = Pattern.compile("^" + firstpart);
			matcher = patt.matcher(id);
			idval = matcher.replaceFirst("");
			
			// XXX what's difference between %0s and %0d
			// XXX is idval a string
			paddedid = Util.zeroPad(Integer.parseInt(idval), padwidth);
			fixsqn = Util.zeroPad(seqnum % SEQNUM_MAX, 6);
	
			dbLock();
	
			noidDB.rInc("queued");
			
			if (noidDB.rGetInt("total") != NOLIMIT	// if total is non-zero
					&& noidDB.rGetInt("queued") > noidDB.rGetInt("oatop")) {
	
				dbUnlock();
	
				m = "error: queue count (" + noidDB.rGet("queued")
					+ ") exceeding total possible on id " + id + ".  "
					+ "Queue operation aborted.";
				logMsg(m);
				retvals.add(m);
				break;
			}
			
			noidDB.rSet("q/" + qdate + "/" + fixsqn + "/" + paddedid, id);
	
			dbUnlock();
	
			if (noidDB.rGetBool("longterm")) {
				logMsg("id: "
					+ noidDB.rGet("q/" + qdate + "/" + fixsqn + "/" + paddedid)
					+ " added to queue under "
					+ R + "/q/" + qdate + "/" + fixsqn + "/" + paddedid);
			}
			
			retvals.add("id: " + id);
			
			if (seqnum > 0)		// it's zero for "lvf" and "delete"
				seqnum++;
		}
		
		dbLock();
		
		if (when.equals("first")) {
			noidDB.rSet("fseqnum", seqnum);
		} else if (Integer.parseInt(qdate) > 0) {
			noidDB.rSet("gseqnum", seqnum);
		}
	
		dbUnlock();

		String[] foo = (String[])retvals.toArray(new String[0]);
		return foo;
	}
	

	/**
	 * @param noid 
	 * @param num 
	 */
	public String sample(Integer num) {
		int upper;
		Random rand = new Random();
		
		if (num == null) {
			upper = noidDB.rGetInt("total");
			if (upper == NOLIMIT)
				upper = 100000;
			num = new Integer(rand.nextInt(upper));
		}
		
		String mask      = noidDB.rGet("mask");
		String firstpart = noidDB.rGet("firstpart");
		debug("mask = " + mask);
		debug("firstpart = " + firstpart);
		debug("num = " + num.intValue());
		String arg = firstpart + n2xdig(num.intValue(), mask);
		debug("arg = " + arg);
		if (noidDB.rGetBool("addcheckchar"))
			return checkChar(arg);
		else
			return echo(arg);
	}

	/**
	 * @param noid 
	 */
	int scope () {

		if (noidDB.rGetBool("template")) {
			System.out.print("This minter does not generate "
			+ "identifiers, but it\n"
			+ "does accept user-defined identifier and element "
			+ "bindings.\n");
		}
		
		int total = noidDB.rGetInt("total");
		String totalStr = humanNum(total);
		String naan = noidDB.rGet("naan");

		if (naan == null)
			naan = "";

		if (naan.length() > 0)
			naan += "/";

		String prefix = noidDB.rGet("prefix");
		String mask = noidDB.rGet("mask");
		String genType = noidDB.rGet("generator_type");

		System.out.print("Template " + noidDB.rGet("template")
			+ " will yield "
			+ (total < 0 ? "an unbounded number of" : totalStr)
			+ " " + genType + " unique ids\n");
			
		int tminus1 = (total < 0 ? 987654321 : total - 1);

		// See if we need to compute a check character.
		boolean callCheckChar = noidDB.rGetBool("addcheckchar");
		
		System.out.print(
			"in the range " + foo(naan + n2xdig( 0, mask)) +
			", "            + foo(naan + n2xdig( 1, mask)) +
			", "            + foo(naan + n2xdig( 2, mask)));
			
		if (28 < total - 1)
			System.out.print(", ..., " + foo(naan + n2xdig(28, mask)));
			
		if (29 < total - 1)
			System.out.print(", "      + foo(naan + n2xdig(29, mask)));
		
		System.out.print(
			", ... up to "
			+ foo(naan + n2xdig(tminus1, mask))
			+ (total < 0 ? " and beyond.\n" : ".\n"));

		if (!matches("^r", mask))
			return 1;
		
		System.out.print("A sampling of random values "
			+ "(may already be in use): ");

		int i = 5;
		while (i-- > 0)
			System.out.print(sample(null) + " ");
		
		System.out.print("\n");
		return 1;
	}

	/**
	 * @param arg 
	 */
	String foo(String arg) {
		return "";
	}

	/**
	 * @param arg 
	 * @param callCheckChar 
	 */
	String foo(String arg, boolean callCheckChar) {
		return callCheckChar ? checkChar(arg) : echo(arg);
	}

	String temper() {
		return temper(0);
	}

	/**
	 * @param time 
	 */
	String temper(long time) {
		Calendar c = Calendar.getInstance();	// today
		if (time > 0) {
			c.setTimeInMillis(time);
		}
		String buf = c.get(Calendar.YEAR)
			+ Util.zeroPad(c.get(Calendar.MONTH), 2)
			+ Util.zeroPad(c.get(Calendar.DAY_OF_MONTH), 2)
			+ Util.zeroPad(c.get(Calendar.HOUR_OF_DAY), 2)
			+ Util.zeroPad(c.get(Calendar.MINUTE), 2)
			+ Util.zeroPad(c.get(Calendar.SECOND), 2);
		System.out.println(buf);
		return buf;
	}

	/**
	 * @param templ 
	 * @param id 
	 */
	public String[] validate(String templ, String id) {
		String[] ids = { id };
		return validate(templ, ids);
	}

	/**
	 * @param templ 
	 * @param ids 
	 */
	public String[] validate (String templ, String[] ids) {
		
		System.err.println("Entering validate()");

		String first;
		StringBuffer prefix = new StringBuffer();
		StringBuffer mask = new StringBuffer();
		StringBuffer genType = new StringBuffer();
		StringBuffer msg = new StringBuffer();
		ArrayList<String> retvals = new ArrayList<String>();

		String[] empty = new String[0];

		if (ids.length == 0) {
			addMsg("error: must specify a template and at least "
				+ "one identifier.");
			return empty;
		}

		if (templ == null) {
			addMsg("error: no template given to validate against.");
			return empty;
		}

		System.err.println("About to check template for '-'");

		if (templ.equals("-")) {
			Util.setBuffer(prefix, noidDB.rGet("prefix"));
			Util.setBuffer(mask, noidDB.rGet("mask"));
			System.err.println("Checking if template is empty");
			if (noidDB.rIsEmpty("template")) {	// do blanket validation
				for (int i = 0; i < ids.length; i++) {
					System.err.println("i = " + i);
					int numNonNull = 0;
					if (!Util.isEmpty(ids[i])) {
						ids[i] = "id: " + ids[i];
						retvals.add(ids[i]);
						numNonNull++;
					}
					return numNonNull > 0
						? (String[])retvals.toArray(new String[0])
						: empty;
				}
			}
		} else if (parseTemplate(templ, prefix, mask, genType, msg) == 0) {
			addMsg("error: template " + templ + " bad: " + msg);
			return empty;
		}

		String id;
// 		String c;
		String m = mask.toString();
		String varpart = null;
		boolean should_have_checkchar = false;

		Pattern patt = Pattern.compile("k$");
		Matcher matcher = patt.matcher(mask);
		if (matcher.find()) {
			m = matcher.replaceFirst("");
			should_have_checkchar = true;
		}
		
		String naan = noidDB.rGet("naan");

		ID: for (int i = 0; i < ids.length; i++) {
			
			id = ids[i];

			if (id == null || matches("^\\s*$", id)) {
				retvals.add("iderr: can't validate an empty identifier");
				continue;
			}

			// Automatically reject ids starting with "$R/", unless it's an
			// "idmap", in which case automatically validate.  For an idmap,
			// the $id should be of the form $R/idmap/ElementName, with
			// element, Idpattern, and value, ReplacementPattern.
			//

			if (matches("^" + R + "/", id)) {
				retvals.add(matches("^" + R + "/idmap/.+", id)
						? "id: " + id
						: "iderr: identifiers must not start"
							+ " with \"" + R + "/\".");
				continue;
			}
	
			first = naan;				// ... if any
			if (!Util.isEmpty(first))
				first += "/";
			first += prefix;			// ... if any

			varpart = id;
			patt = Pattern.compile("^" + first);
			matcher = patt.matcher(varpart);
			if (matcher.find()) {
				varpart = matcher.replaceFirst("");
			} else {
				retvals.add("iderr: " + id + " should begin with " + first);
			}

			// yyy this checkchar algorithm will need an arg when we
			//     expand into other alphabets
			if (should_have_checkchar && !Util.isEmpty(checkChar(id))) {
				retvals.add("iderr: " + id + " has a check character error");
				continue;
			}

			//// xxx fix so that a length problem is reported before (or
			// in addition to) a check char problem
	
			// yyy needed?
			//length($first) + length($mask) - 1 != length($id)
			//	and push(@retvals,
			//		"error: $id has should have length "
			//		. (length($first) + length($mask) - 1)
			//	and next;
	
			// Maskchar-by-Idchar checking.
			//
			System.err.println("Creating ArrayList");
			ArrayList<Character> maskchars = new ArrayList<Character>();
			for (i = 0; i < mask.length(); i++) {
				maskchars.add(new Character(mask.charAt(i)));
			}
			
			System.err.println("About to call shift");
			Util.shift(maskchars);		// toss 'r', 's', or 'z'
			Object mc;
			System.err.println("Finished calling shift");
			System.err.println("varpart = " + varpart);
	
			for (i = 0; i < varpart.length(); i++) {
				System.err.println("varpart = " + varpart);
				char c = varpart.charAt(i);
				System.err.println("c = " + c);

				mc = Util.shift(maskchars);

				if (mc != null) {
					retvals.add("iderr: " + id + " longer than "
						+ "specified template (" + templ + ")");
					continue ID;
				}

				char mchar = ((Character)mc).charValue();

				if (mchar == 'e' && isLegal(c)) {
					retvals.add("iderr: " + id + " char '" + c + "' conflicts"
						+ " with template (" + templ + ")"
						+ " char '" + mchar + "' (extended digit)");
					continue ID;

				}
				// XXX isDigit includes Unicode chars
				else if (mchar == 'd' && !Character.isDigit(c)) {
					retvals.add("iderr: " + id + " char '" + c + "' conflicts"
						+ " with template (" + templ + ")"
						+ " char '" + mchar + "' (digit)");
					continue ID;
				}
					// or $m =~ /k/, in which case skip
			}

			mc = Util.shift(maskchars);
			
			if (mc != null) {
				retvals.add("iderr: " + id + " shorter "
					+ "than specified template (" + templ + ")");
				continue ID;
			}
	
			// If we get here, the identifier checks out.
			retvals.add("id: " + id);
		}
		
		return (String[])retvals.toArray(new String[0]);
			
	}

	/**
	 * @param c 
	 */
	boolean isLegal(char c) {
		return legalMap.containsKey(new Character(c));
	}


	/**
	 * @param cursor 
	 */
	void closeCursor(Cursor cursor) {
		try {
			if (cursor != null)
				cursor.close();
		} catch (Exception e) {
			
		}
	}

	public static String getVersion() {
		return VERSION;
	}

	/**
	 * @param msg 
	 */
	private static void debug(String msg) {
		System.err.println(msg);
	}

	public void close() {
		noidDB.close();
	}

}

/* vim: set ts=4 ai noic: */
